﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TopDownOperatorPrecedence
{


    public static class VisitorExtensions
    {
        public static PObject Walk(this PNode node, PEnvironment environment)
        {
            if (node.ID == "(literal)")
            {
                switch (node.Type)
                {
                    case "number":
                        return new PNumber(int.Parse(node.Value));
                    case "string":
                        return new PString(node.Value);
                    case "bool":
                        return node.Value.ToUpper() == "TRUE" ? PBool.True : PBool.False;
                    default:
                        throw new Exception("Unrecognized literal type");
                }
            }
            else if (node.Arity == "binary")
            {
                switch (node.ID)
                {
                    case "+":
                    case "-":
                    case "*":
                    case "/":
                    case "%":
                    case "+=":
                    case "-=":
                    case "*=":
                    case "/=":
                    case "%=":
                    case "[":
                    case "==":
                    case "!=":
                    case "<":
                    case ">":
                    case "<=":
                    case ">=":
                        {
                            PObject first = Walk(node.First, environment);
                            PObject second = Walk(node.Second, environment);
                            return first.InvokeBinaryOperation(node.ID, second);
                        }
                    case ".":
                        {
                            PObject first = Walk(node.First, environment);
                            PObject name = new PString(node.Second.Value);
                            return first.DotGet(name);
                        }
                    case "(":
                        {
                            if (node.First.ID == "(name)")
                            {
                                if (node.First.Value == "range")
                                {
                                    List<PObject> parameters = new List<PObject>();
                                    PNode parameterNode = node.Second;
                                    if (parameterNode != null)
                                    {
                                        if (parameterNode is PNodeArray)
                                        {
                                            foreach (var item in (parameterNode as PNodeArray).ArrayObject)
                                            {
                                                var value = item.Walk(environment);
                                                parameters.Add(value);
                                            }
                                        }
                                        else
                                        {
                                            var value = parameterNode.Walk(environment);
                                            parameters.Add(value);
                                        }
                                    }
                                    int[] numbers = parameters.Select(p => (p as PNumber).Value).ToArray();
                                    switch (numbers.Length)
                                    {
                                        case 1:
                                            return new PList(Enumerable.Range(0, numbers[0]).Select(i => new PNumber(i)).Cast<PObject>().ToList());
                                        case 2:
                                            return new PList(Enumerable.Range(numbers[0], numbers[1] - numbers[0] + 1).Select(i => new PNumber(i)).Cast<PObject>().ToList());
                                        default:
                                            throw new Exception("invalid range input");
                                    }
                                }
                                else if (node.First.Value == "object")
                                {
                                    if (node.Second == null)
                                    {
                                        return new PObject();
                                    }
                                    else
                                    {
                                        PNodeArray paras = node.Second as PNodeArray;
                                        if (paras.ArrayObject.Count == 0)
                                        {
                                            return new PObject();
                                        }
                                        else
                                        {
                                            var result = paras.ArrayObject[0].Walk(environment);
                                            return result.Clone();
                                        }
                                    }
                                    throw new Exception("incorrect object initialization");
                                }
                            }

                            PFunction func = node.First.Walk(environment) as PFunction;
                            if (func == null)
                            {
                                throw new Exception("Expect a callable object");
                            }
                            else
                            {
                                List<PObject> parameters = new List<PObject>();
                                PNode parameterNode = node.Second;
                                if (parameterNode != null)
                                {
                                    if (parameterNode is PNodeArray)
                                    {
                                        foreach (var item in (parameterNode as PNodeArray).ArrayObject)
                                        {
                                            var value = item.Walk(environment);
                                            parameters.Add(value);
                                        }
                                    }
                                    else
                                    {
                                        var value = parameterNode.Walk(environment);
                                        parameters.Add(value);
                                    }
                                }
                                return func.Invoke(parameters, environment);
                            }
                        }
                    case "=":
                        {
                            if (node.First.ID == "(name)")
                            {
                                string name = node.First.Value;
                                PObject second = Walk(node.Second, environment);
                                if (node.IsAssignment)
                                {
                                    environment.Assign(name, second);
                                }
                                else
                                {
                                    environment.Define(name, second);
                                }
                                return second;
                            }
                            else if (node.First.ID == "[") // an array assign
                            {
                                string name = node.First.Value;
                                PObject second = Walk(node.Second, environment);
                                PObject array = node.First.First.Walk(environment);
                                PObject index = node.First.Second.Walk(environment);
                                array.IndexSet(index, second);
                                return second;
                            }
                            else if (node.First.ID == ".") // a property assign
                            {
                                PObject instance = node.First.First.Walk(environment);
                                string propName = node.First.Second.Value;
                                PObject value = node.Second.Walk(environment);
                                if (value is PFunction)
                                {
                                    (value as PFunction).Parameters.Insert(0, "this");
                                }
                                instance.DotSet(new PString(propName), value);
                                return value;
                            }
                        }
                        break;
                    default:
                        break;
                }
            }
            else if (node.Arity == "unary")
            {
                switch (node.ID)
                {
                    case "[":
                        {
                            PNodeArray listNode = node.First as PNodeArray;
                            if (listNode == null)
                            {
                                throw new Exception("Expect a list");
                            }
                            else
                            {
                                List<PObject> values = new List<PObject>();
                                foreach (var item in listNode.ArrayObject)
                                {
                                    var value = item.Walk(environment);
                                    values.Add(value);
                                }
                                return new PList(values);
                            }
                        }
                    case "{":
                        {
                            if (node.Type == "object")
                            {
                                PObject result = new PObject();
                                PNodeArray props = node.First as PNodeArray;
                                foreach (var item in props.ArrayObject)
                                {
                                    var value = item.Walk(environment);
                                    result.Members.Define(item.Key, value);
                                    if (value is PFunction)
                                    {
                                        (value as PFunction).Parameters.Insert(0, "this");
                                    }
                                }
                                return result;
                            }
                        }
                        break;
                    case "+":
                    case "-":
                        {
                            PObject result = node.First.Walk(environment);
                            if (result is PNumber)
                            {
                                int value = (result as PNumber).Value;
                                if (node.ID == "-")
                                {
                                    value = -value;
                                }
                                return new PNumber(value);
                            }
                            throw new Exception("- + must apply on number");
                        }
                    case "typeof":
                    default:
                        break;
                }
            }
            else if (node.Arity == "statement")
            {
                switch (node.ID)
                {
                    case "while":
                        {
                            PNode condNode = node.First;
                            PNode statNode = node.Second;
                            while ((condNode.Walk(environment) as PBool) == PBool.True)
                            {
                                try
                                {
                                    environment = new PEnvironment(environment);
                                    if (statNode != null)
                                    {
                                        if (statNode is PNodeArray)
                                        {
                                            foreach (var item in (statNode as PNodeArray).ArrayObject)
                                            {
                                                item.Walk(environment);
                                            }
                                        }
                                        else
                                        {
                                            statNode.Walk(environment);
                                        }
                                    }
                                }
                                catch (PBreakException)
                                {
                                    break;
                                }
                                finally
                                {
                                    environment = environment.Parent;
                                }
                            }
                        }
                        break;
                    case "if":
                        {
                            // first is condition
                            // second is statements
                            // third is a if/statments/none
                            PNode current = node;
                            while (current != null && current.ID == "if" && (current.First.Walk(environment) as PBool) != PBool.True)
                            {
                                current = current.Third;
                            }
                            // go to the right branch
                            if (current != null)
                            {
                                PNode statNode = null;
                                if (current.ID == "if")
                                {
                                    statNode = current.Second;
                                }
                                else
                                {
                                    statNode = current;
                                }

                                try
                                {
                                    environment = new PEnvironment(environment);
                                    if (statNode != null)
                                    {
                                        if (statNode is PNodeArray)
                                        {
                                            foreach (var item in (statNode as PNodeArray).ArrayObject)
                                            {
                                                item.Walk(environment);
                                            }
                                        }
                                        else
                                        {
                                            statNode.Walk(environment);
                                        }
                                    }
                                }
                                finally
                                {
                                    environment = environment.Parent;
                                }
                            }
                        }
                        break;
                    case "for":
                        {
                            List<PNode> statNodes = (node.First as PNodeArray).ArrayObject;
                            PNode blockNode = node.Second;
                            if (statNodes.Count != 3)
                            {
                                throw new Exception("Invalid for statement");
                            }
                            try
                            {
                                environment = new PEnvironment(environment);
                                statNodes[0].Walk(environment);
                                while ((statNodes[1].Walk(environment) as PBool) == PBool.True)
                                {
                                    if (blockNode != null)
                                    {
                                        try
                                        {
                                            environment = new PEnvironment(environment);
                                            if (blockNode is PNodeArray)
                                            {
                                                foreach (var item in (blockNode as PNodeArray).ArrayObject)
                                                {
                                                    item.Walk(environment);
                                                }
                                            }
                                            else
                                            {
                                                blockNode.Walk(environment);
                                            }
                                        }
                                        catch (PBreakException)
                                        {
                                            break;
                                        }
                                        finally
                                        {
                                            environment = environment.Parent;
                                        }
                                    }
                                    statNodes[2].Walk(environment);
                                }
                            }
                            finally
                            {
                                environment = environment.Parent;
                            }
                        }
                        break;
                    case "break":
                        throw new PBreakException();
                    case "foreach":
                        {
                            string[] loopnames = (node.First.First as PNodeArray).ArrayObject.Select(v => v.Value).ToArray();
                            PObject listObject = node.First.Second.Walk(environment);

                            if (loopnames.Length == 1 && listObject is PList)
                            {
                                try
                                {
                                    environment = new PEnvironment(environment);
                                    environment.Define(loopnames[0], PObject.Null);
                                    PList actualList = listObject as PList;
                                    PNode body = node.Second;
                                    if (body != null)
                                    {
                                        foreach (var item in actualList.Value)
                                        {
                                            environment.Assign(loopnames[0], item);
                                            try
                                            {
                                                environment = new PEnvironment(environment);
                                                if (body is PNodeArray)
                                                {
                                                    foreach (var stat in (body as PNodeArray).ArrayObject)
                                                    {
                                                        stat.Walk(environment);
                                                    }
                                                }
                                                else
                                                {
                                                    body.Walk(environment);
                                                }
                                            }
                                            catch (PBreakException)
                                            {
                                                break;
                                            }
                                            finally
                                            {
                                                environment = environment.Parent;
                                            }
                                        }
                                    }
                                }
                                finally
                                {
                                    environment = environment.Parent;
                                }
                            }
                        }
                        break;
                    case "return":
                        {
                            if (node.First == null)
                            {
                                throw new PReturnException(PObject.Null);
                            }
                            else
                            {
                                PObject result = node.First.Walk(environment);
                                throw new PReturnException(result);
                            }
                        }
                    case "print":
                        {
                            PObject result = node.First.Walk(environment);
                            Console.WriteLine(result.ToRep());
                        }
                        break;
                    default:
                        throw new Exception("Unrecognized statement " + node.ID);
                }
            }
            else if (node.Arity == "function")
            {
                PNode parameterNode = node.First;
                List<string> parameters = new List<string>();
                if (parameterNode is PNodeArray)
                {
                    foreach (var item in (parameterNode as PNodeArray).ArrayObject)
                    {
                        if (item.ID == "(name)")
                        {
                            parameters.Add(item.Value);
                        }
                        else
                        {
                            throw new Exception("Invalid function parameter name");
                        }
                    }
                }
                else
                {
                    parameters.Add(parameterNode.Value);
                }
                PNode bodyNode = node.Second;
                PFunction function = new PFunction(parameters, bodyNode);
                return function;
            }
            else if (node.ID == "this")
            {
                return environment.Lookup("this");
            }
            else if (node.ID == "(name)")
            {
                PObject value = environment.Lookup(node.Value);
                if (value == null)
                {
                    throw new Exception(node.Value + " not defined");
                }
                else
                {
                    return value;
                }
            }
            else if (node.Arity == "ternary")
            {
                if (node.ID == "(")
                {
                    PObject instance = node.First.Walk(environment);
                    PObject method = instance.DotGet(new PString(node.Second.Value));
                    List<PObject> parameters = new List<PObject>();
                    parameters.Add(instance); // for this
                    if (node.Third is PNodeArray)
                    {
                        foreach (var item in (node.Third as PNodeArray).ArrayObject)
                        {
                            parameters.Add(item.Walk(environment));
                        }
                    }
                    else
                    {
                        parameters.Add(node.Third.Walk(environment));
                    }
                    PObject result = method.Invoke(parameters, environment);
                    return result;
                }
                else if (node.ID == "?")
                {

                }
            }
            return null;
        }
    }
}
